#include "PreCompile.hpp"
#include "TokenizerInput.hpp"

namespace CPreProc
{
  typedef std::string LineReaderException;

  ////////
  // TokenizerInput
  Glib::RefPtr<TokenizerInput> TokenizerInput::create(BuildNG::InStream &src)
  {
    return Glib::RefPtr<TokenizerInput>(new TokenizerInput(src));
  }

  Glib::RefPtr<TokenizerInput> TokenizerInput::create(const char *src)
  {
    BuildNG::MemoryInStream stream(src);
    return Glib::RefPtr<TokenizerInput>(new TokenizerInput(stream));
  }

  Glib::RefPtr<TokenizerInput> TokenizerInput::create(const Glib::ustring &src)
  {
    BuildNG::MemoryInStream stream(src.c_str());
    return Glib::RefPtr<TokenizerInput>(new TokenizerInput(stream));
  }

  TokenizerInput::TokenizerInput(BuildNG::InStream &src)
  {
    LineReader reader(src);
    while(1)
    {
      LogicalLine *cur_line = new LogicalLine;
      try
      {
        if(!reader.read_logical_line(*cur_line))
        {
          delete cur_line;
          break;
        }

        lines.push_back(cur_line);
        cur_line = 0;
      }
      catch(...)
      {
        delete cur_line;
        throw;
      }
    }
  }

  TokenizerInput::~TokenizerInput()
  {
    for(int i = 0;i < (int)lines.size();i++)
    {
      delete lines[i];
    }
  }

  TokenizerInput::LineIterator TokenizerInput::get_line_iterator() const
  {
    LineIterator ret;
    ret.doc = this_ptr<TokenizerInput>();
    ret.i = -1;
    return ret;    
  }

  SourceLocation TokenizerInput::get_source_location(const char *c,int tab_size) const
  {
    // find the logical line this char is a part of
    for(int i = 0;i < (int)lines.size();i++)
    {
      if(c >= lines[i]->get_begin() && c < lines[i]->get_end())
      {
        return lines[i]->get_source_location(c,tab_size);
      }
    }

    assert("That character wasn't a part of this source file.");
    return SourceLocation(-1,-1);
  }

  ////////
  // TokenizerInput::LineIterator
  bool TokenizerInput::LineIterator::move_next()
  {
    i++;
    return i < (int)doc->lines.size();
  }

  const char* TokenizerInput::LineIterator::get_line_begin() const
  {
    assert(i >= 0 && i < (int)doc->lines.size());
    return doc->lines[i]->get_begin();
  }

  const char* TokenizerInput::LineIterator::get_line_end() const
  {
    assert(i >= 0 && i < (int)doc->lines.size());
    return doc->lines[i]->get_end();
  }

  Glib::ustring TokenizerInput::LineIterator::to_string() const
  {
    assert(i >= 0 && i < (int)doc->lines.size());
    return doc->lines[i]->to_string();
  }

  ///////
  // TokenizerInput::LineReader
  TokenizerInput::LineReader::LineReader(BuildNG::InStream &in)
    : in(in),
    in_comment_block(false),
    cur_phys_line(0)
  {
  }

  bool TokenizerInput::LineReader::read_logical_line(LogicalLine &logical_line)
  {
    logical_line.clear();
    logical_line.first_physical_line = cur_phys_line;

    BuildNG::StringBuilder<256> logical_line_str;

    bool last_was_backslash = false;
    bool anything_read = false;
    do
    {
      Glib::ustring physical_line;

      bool read_res = in.read_line(physical_line);
      cur_phys_line++;
      if(!read_res)
      {
        if(last_was_backslash)
          throw LineReaderException("Unexpected end of file. Line to concat expected.");

        if(!anything_read)
          return false;

        // break out of the reading loop, and do the final comment stripping.
        break;
      }
      anything_read = true;

      process_trigraphs_and_append(physical_line,logical_line_str,logical_line);

      // if the last char is a \, we have to read the next physical line aswell,
      // and append it to this logical line.
      if(*(logical_line_str.get_end() - 1) == '\\')
      {
        // pop the \ from the end of the line.
        logical_line_str.pop_end(1);
        last_was_backslash = true;
      }
      else
        last_was_backslash = false;
    }
    while(last_was_backslash);

    strip_comments(logical_line_str,logical_line);

    // copy the string to the logical_line's "begin" and "end" members.
    logical_line.begin = new char[logical_line_str.get_size()];
    logical_line.end = logical_line.begin + logical_line_str.get_size();
    memcpy(logical_line.begin,logical_line_str.get_begin(),logical_line_str.get_size());

    std::stable_sort(
      logical_line.replacement_evidence.get_begin(),
      logical_line.replacement_evidence.get_end(),
      LogicalLine::ReplacementEvidence::compare_pos);

    return true;

  }

  void TokenizerInput::LineReader::process_trigraphs_and_append(const Glib::ustring &str,
    BuildNG::StringBuilder<256> &logical_line_str,LogicalLine &logical_line)
  {
    const gchar *read_ptr = str.c_str();

    while(*read_ptr)
    {
      if(*read_ptr == '?' && *(read_ptr + 1) == '?')
      {
        switch(*(read_ptr + 2))
        {
        case '=':
          logical_line_str.append('#');
          logical_line.add_replacement_evidence(logical_line_str.get_size(),2,0);
          read_ptr += 3;
          break;
        case '/':
          logical_line_str.append('\\');
          logical_line.add_replacement_evidence(logical_line_str.get_size(),2,0);
          read_ptr += 3;
          break;
        case '\'':
          logical_line_str.append('^');
          logical_line.add_replacement_evidence(logical_line_str.get_size(),2,0);
          read_ptr += 3;
          break;
        case '(':
          logical_line_str.append('[');
          logical_line.add_replacement_evidence(logical_line_str.get_size(),2,0);
          read_ptr += 3;
          break;
        case ')':
          logical_line_str.append(']');
          logical_line.add_replacement_evidence(logical_line_str.get_size(),2,0);
          read_ptr += 3;
          break;
        case '!':
          logical_line_str.append('|');
          logical_line.add_replacement_evidence(logical_line_str.get_size(),2,0);
          read_ptr += 3;
          break;
        case '<':
          logical_line_str.append('{');
          logical_line.add_replacement_evidence(logical_line_str.get_size(),2,0);
          read_ptr += 3;
          break;
        case '>':
          logical_line_str.append('}');
          logical_line.add_replacement_evidence(logical_line_str.get_size(),2,0);
          read_ptr += 3;
          break;
        case '-':
          logical_line_str.append('~');
          logical_line.add_replacement_evidence(logical_line_str.get_size(),2,0);
          read_ptr += 3;
          break;

        default:
          logical_line_str.append(*(read_ptr++));
          break;
        }
      }
      else
      {
        logical_line_str.append(*(read_ptr++));
      }
    }
  }

  void TokenizerInput::LineReader::strip_comments(BuildNG::StringBuilder<256> &logical_line_str,LogicalLine &logical_line)
  {
    gchar *write_ptr = logical_line_str.get_begin();
    gchar *read_ptr = write_ptr;
    gchar *end = logical_line_str.get_end();
    
    // add 2 dummy chars, to make sure we never read from unallocated memory.
    // These chars will be removed by the truncate at the end of this function.
    logical_line_str.append("  ");
    
    char expected_closing_quote = 0;

    while(read_ptr != end)
    {
      if(expected_closing_quote)
      { 
        if(*read_ptr == expected_closing_quote)
          expected_closing_quote = 0;
        *(write_ptr++) = *(read_ptr++);
      }
      else if(!in_comment_block)
      {
        if(*read_ptr == '/' && *(read_ptr + 1) == '/')
        {
          // it's a // comment, so we will cut off the rest of the line.
          // (this will be done automatically, outside this while loop).
          break;
        }
        else if(*read_ptr == '/' && *(read_ptr + 1) == '*')
        {
          // it's the beginning of a /* comment
          read_ptr += 2;
          in_comment_block = true;
        }
        else if(*read_ptr == '\"' || *read_ptr == '\'')
        {   
          expected_closing_quote = *read_ptr;
          *(write_ptr++) = *(read_ptr++);
        }
        else
        {
          *(write_ptr++) = *(read_ptr++);
        }
      }
      else
      {
        if(*read_ptr == '*' && *(read_ptr + 1) == '/')
        {
          in_comment_block = false;
          // write a space for the whole comment block, as whitespace has the
          // same lexical function as a comment block.
          *(write_ptr++) = ' ';
          read_ptr += 2;
        }
        else
        {
          read_ptr++;
        }
      }
    }

    logical_line_str.truncate_to(write_ptr);
  }

  bool TokenizerInput::LineReader::end_of_file() const
  {
    assert(!"not implemented yet");
    return false;
  }

  /////////
  // TokenizerInput::LogicalLine
  TokenizerInput::LogicalLine::LogicalLine()
  {
    begin = end = 0;
  }

  TokenizerInput::LogicalLine::~LogicalLine()
  {
    delete [] begin;
  }

  void TokenizerInput::LogicalLine::add_replacement_evidence(int logical_line_pos,int num_chars_right,int num_tabs_right)
  {
    ReplacementEvidence evidence;
    evidence.logical_line_pos = logical_line_pos;
    evidence.num_chars_right = num_chars_right;
    evidence.num_tabs_right = num_tabs_right;
    replacement_evidence.append(evidence);
  }

  Glib::ustring TokenizerInput::LogicalLine::to_string() const
  {
    return Glib::ustring(begin,end - begin);
  }

  void TokenizerInput::LogicalLine::clear()
  {
    replacement_evidence.clear();
    
    delete [] begin;
    begin = end = 0;
  }

  SourceLocation TokenizerInput::LogicalLine::get_source_location(const char *logical_pos,int tab_size) const
  {
    assert(logical_pos >= begin && logical_pos < end);

    SourceLocation res(first_physical_line,0);

    int next_replacement_evidence = 0;
    for(const char *c = begin;c != logical_pos;c++)
    {
      while(
        next_replacement_evidence < replacement_evidence.get_size() &&
        c - begin == replacement_evidence[next_replacement_evidence].logical_line_pos)
      {
        res.pos += replacement_evidence[next_replacement_evidence].num_chars_right;
        res.pos += replacement_evidence[next_replacement_evidence].num_tabs_right * tab_size;

        next_replacement_evidence++;
      }

      switch(*c)
      {
      case '\t':
        res.pos += tab_size;
        break;
      case '\n':
        res.pos = 0;
        res.line++;
        break;

      default:
        res.pos++;
      }
    }

    while(
      next_replacement_evidence < replacement_evidence.get_size() &&
      logical_pos - begin == replacement_evidence[next_replacement_evidence].logical_line_pos)
    {
      res.pos += replacement_evidence[next_replacement_evidence].num_chars_right;
      res.pos += replacement_evidence[next_replacement_evidence].num_tabs_right * tab_size;

      next_replacement_evidence++;
    }

    return res;
  }
}
